/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package TweetRay;

import Raytracing.Shaders.Material;
import Raytracing.Shaders.Shader;
import Raytracing.Geometry.Triangle;
import java.util.ArrayList;
import java.util.Arrays;
import javax.print.attribute.standard.PDLOverrideSupported;
import processing.core.PApplet;
import processing.core.PGraphics3D;
import processing.core.PVector;

/**
 *
 *           __
 *          /  \
 *          ^  ^
 *         / \/ \
 *         | .| .|
 *         \_/\_/
 *          |  |
 *          \ o/
 *          /  \
 *         |    |
 *         | | ||
 *         | 0 0|
 *         |    |
 */
public class Box extends Entity implements Traceable {

    public float rx, ry, rz;
    public float sx, sy, sz;
    public PVector[] pts = new PVector[8];
    public Triangle[] tris = new Triangle[12];
    boolean outlined = false;

    public Box(int x, int y, int z,
            float sx, float sy, float sz,
            int rx, int ry, int rz,
            Material[] shaders, int shaderIndex) {
        this(x, y, z, sx, sy, sz, rx, ry, rz, shaders, shaderIndex, 0);
    }

    public Box(int x, int y, int z,
            float sx, float sy, float sz,
            int rx, int ry, int rz,
            Material[] shaders, int shaderIndex, int group) {


        super(x, y, z);

        this.group = group;

        //scale is stored with only 7 bits precision.... so decompress here
        sx *= 2;
        sy *= 2;
        sz *= 2;

        this.sx = sx;
        this.sy = sy;
        this.sz = sz;


        this.shaders = shaders;
        this.shaderIndex = shaderIndex;


        pts[0] = new PVector(tx - sx / 2, ty - sy / 2, tz + sz / 2);
        pts[1] = new PVector(tx - sx / 2, ty - sy / 2, tz - sz / 2);
        pts[2] = new PVector(tx + sx / 2, ty - sy / 2, tz - sz / 2);
        pts[3] = new PVector(tx + sx / 2, ty - sy / 2, tz + sz / 2);

        pts[4] = new PVector(tx - sx / 2, ty + sy / 2, tz + sz / 2);
        pts[5] = new PVector(tx - sx / 2, ty + sy / 2, tz - sz / 2);
        pts[6] = new PVector(tx + sx / 2, ty + sy / 2, tz - sz / 2);
        pts[7] = new PVector(tx + sx / 2, ty + sy / 2, tz + sz / 2);


        PVector trans = new PVector(tx, ty, tz);
        PVector dir = trans.get();
        dir.z += 1;
        rotate(trans, dir, int2Rot(rz));
        dir = trans.get();
        dir.y += 1;
        rotate(trans, dir, int2Rot(ry));
        dir = trans.get();
        dir.x += 1;
        rotate(trans, dir, int2Rot(rx));


        tesselate();

    }

    public Box copy() {
        System.out.println("copying: " + sx + ":" + sy + ":" + sz);
        return new Box((int) tx, (int) ty, (int) tz,
                (int) sx / 2, (int) sy / 2, (int) sz / 2,
                (int) rot2Int(rx), (int) rot2Int(ry), (int) rot2Int(rz),
                shaders, shaderIndex);
    }

    public void drawManipulators(PGraphics3D p3d) {

        p3d.pushMatrix();

        p3d.stroke(255);
        p3d.noFill();

        p3d.ellipse(0, 0, 5, 5);


        p3d.translate(2 * sx, 0, 0);



        p3d.box(1, 1, 1);
        p3d.popMatrix();

        p3d.noStroke();

    }

    public void drawOutline(PGraphics3D p3d) {


        p3d.line(pts[0].x, pts[0].y, pts[0].z, pts[1].x, pts[1].y, pts[1].z);
        p3d.line(pts[0].x, pts[0].y, pts[0].z, pts[3].x, pts[3].y, pts[3].z);
        p3d.line(pts[0].x, pts[0].y, pts[0].z, pts[4].x, pts[4].y, pts[4].z);

        p3d.line(pts[2].x, pts[2].y, pts[2].z, pts[1].x, pts[1].y, pts[1].z);
        p3d.line(pts[2].x, pts[2].y, pts[2].z, pts[3].x, pts[3].y, pts[3].z);
        p3d.line(pts[2].x, pts[2].y, pts[2].z, pts[6].x, pts[6].y, pts[6].z);

        p3d.line(pts[7].x, pts[7].y, pts[7].z, pts[4].x, pts[4].y, pts[4].z);
        p3d.line(pts[7].x, pts[7].y, pts[7].z, pts[3].x, pts[3].y, pts[3].z);
        p3d.line(pts[7].x, pts[7].y, pts[2].z, pts[6].x, pts[6].y, pts[6].z);

        p3d.line(pts[7].x, pts[7].y, pts[7].z, pts[4].x, pts[4].y, pts[4].z);
        p3d.line(pts[7].x, pts[7].y, pts[7].z, pts[3].x, pts[3].y, pts[3].z);
        p3d.line(pts[7].x, pts[7].y, pts[2].z, pts[6].x, pts[6].y, pts[6].z);


    }

    public void pickDraw(PGraphics3D p3d, int index) {
        p3d.pushMatrix();

        p3d.translate(tx, ty, tz);

        p3d.rotateX(rx);
        p3d.rotateY(ry);
        p3d.rotateZ(rz);

        p3d.scale(sx, sy, sz);

        p3d.fill(index);
        p3d.box(1, 1, 1);
        p3d.popMatrix();

    }

    public void draw(PGraphics3D p3d) {


        p3d.pushMatrix();

        p3d.stroke(255, 255, 255, 128);
        p3d.line(tx, ty, tz, tx, 255, tz);

        p3d.noStroke();

        p3d.translate(tx, ty, tz);

        p3d.rotateX(rx);
        p3d.rotateY(ry);
        p3d.rotateZ(rz);

        p3d.scale(sx, sy, sz);





        p3d.fill(shaders[shaderIndex].fill);
        p3d.box(1, 1, 1);
        if (isSelected) {
            p3d.stroke(255, 255, 0);
            p3d.noFill();
            p3d.box(1.05f, 1.05f, 1.05f);


            //manipulators
            p3d.stroke(250, 250, 250);

            p3d.fill(255, 255, 255);


            int x = (int) p3d.screenX(1.25f, 0, 0);
            int y = (int) p3d.screenY(1.25f, 0, 0);
            p3d.line(0, 0, 0, 1, 0, 0);
            p3d.text("X - " + (int)sx, x, y);

            x = (int) p3d.screenX(0, 1.25f, 0);
            y = (int) p3d.screenY(0, 1.25f, 0);
            p3d.line(0, 0, 0, 0, 1, 0);
            p3d.text("Y - " + (int)sy, x, y);

            x = (int) p3d.screenX(0,0, 1.25f);
            y = (int) p3d.screenY(0,0, 1.25f);
            p3d.line(0, 0, 0, 0, 0, 1);
            p3d.text("Z - " + (int)sz, x, y);            
            
            
            p3d.noStroke();

        }

        p3d.popMatrix();

        /*
        if (!isSelected) {
        for (int i = 0; i < tris.length; i++) {
        tris[i].draw(p3d.parent);
        }
        }
         */





    }

    public void translate(PVector dir) {

        if (tx + dir.x <= 255 && tx + dir.x >= 0) {

            tx += dir.x;
            for (int i = 0; i < pts.length; i++) {
                pts[i].x += dir.x;
            }
        }

        if (ty + dir.y <= 255 && ty + dir.y >= 0) {

            ty += dir.y;
            for (int i = 0; i < pts.length; i++) {
                pts[i].y += dir.y;
            }
        }

        if (tz + dir.z <= 255 && tz + dir.z >= 0) {

            tz += dir.z;
            for (int i = 0; i < pts.length; i++) {
                pts[i].z += dir.z;
            }
        }


    }

    public void scale(int axis, float amt) {


        switch (axis) {
            case (0):
                if (sx + amt >= 1 && sx + amt <= 255) {
                    sx += amt;

                    PVector localX = getLocalXAxis(false);
                    float mag = localX.mag();

                    localX.div(mag);
                    localX.mult(amt / 2);

                    pts[3].add(localX);
                    pts[2].add(localX);
                    pts[7].add(localX);
                    pts[6].add(localX);

                    pts[0].sub(localX);
                    pts[1].sub(localX);
                    pts[4].sub(localX);
                    pts[5].sub(localX);
                }
                break;
            case (1):
                if (sy + amt >= 1 && sy + amt <= 255) {
                    sy += amt;

                    PVector localY = getLocalYAxis(false);
                    float mag = localY.mag();

                    localY.div(mag);
                    localY.mult(amt / 2);

                    //System.out.println(localY);

                    pts[4].add(localY);
                    pts[5].add(localY);
                    pts[6].add(localY);
                    pts[7].add(localY);

                    pts[0].sub(localY);
                    pts[1].sub(localY);
                    pts[2].sub(localY);
                    pts[3].sub(localY);
                }
                break;
            case (2):
                if (sz + amt >= 1 && sz + amt <= 255) {
                    sz += amt;

                    PVector localZ = getLocalZAxis(false);
                    float mag = localZ.mag();

                    localZ.div(mag);
                    localZ.mult(amt / 2);

                    pts[0].add(localZ);
                    pts[3].add(localZ);
                    pts[4].add(localZ);
                    pts[7].add(localZ);

                    pts[1].sub(localZ);
                    pts[2].sub(localZ);
                    pts[5].sub(localZ);
                    pts[6].sub(localZ);
                }
                break;

            case (3):
                //I are recursive?
                scale(0, amt);
                scale(1, amt);
                scale(2, amt);
                break;

        }




    }

    public PVector getLocalXAxis(boolean isNormalized) {
        PVector toRet = new PVector(pts[3].x - pts[0].x,
                pts[3].y - pts[0].y,
                pts[3].z - pts[0].z);
        if (isNormalized) {
            toRet.normalize();
        }
        return toRet;
    }

    public PVector getLocalYAxis(boolean isNormalized) {
        PVector toRet = new PVector(pts[4].x - pts[0].x,
                pts[4].y - pts[0].y,
                pts[4].z - pts[0].z);
        if (isNormalized) {
            toRet.normalize();
        }
        return toRet;
    }

    public PVector getLocalZAxis(boolean isNormalized) {
        PVector toRet = new PVector(pts[4].x - pts[5].x,
                pts[4].y - pts[5].y,
                pts[4].z - pts[5].z);
        if (isNormalized) {
            toRet.normalize();
        }
        return toRet;
    }

    public void rotate(PVector origin, PVector direction, float amt) {
        super.rotate(origin, direction, amt);
        for (int i = 0; i < pts.length; i++) {
            pts[i].rotate(origin, direction, amt);
        }




        /*
        
        switch (axis) {
        case (0):
        rx += amt;
        for (int i = 0; i < pts.length; i++) {
        pts[i].rotate(new PVector(tx, ty, tz), new PVector(tx + 1, ty, tz), amt);
        }
        break;
        case (1):
        ry += amt;
        for (int i = 0; i < pts.length; i++) {
        pts[i].rotate(new PVector(tx, ty, tz), new PVector(tx , ty+1, tz), amt);
        }
        break;
        case (2):
        rz += amt;
        for (int i = 0; i < pts.length; i++) {
        pts[i].rotate(new PVector(tx, ty, tz), new PVector(tx , ty, tz+1), amt);
        }
        break;
        //local x axis rotation
        case (TweetRay.LX_AXIS):
        
        PVector localX = getLocalXAxis(true);
        for (int i = 0; i < pts.length; i++) {
        pts[i].rotate(new PVector(tx, ty, tz), new PVector(tx + localX.x, ty + localX.y, tz + localX.z), amt);
        }
        
        break;
        //local x axis rotation
        case (TweetRay.LY_AXIS):
        
        PVector localY = getLocalYAxis(true);
        for (int i = 0; i < pts.length; i++) {
        pts[i].rotate(new PVector(tx, ty, tz), new PVector(tx + localY.x, ty + localY.y, tz + localY.z), amt);
        }
        
        break;
        case (TweetRay.LZ_AXIS):
        
        PVector localZ = getLocalZAxis(true);
        for (int i = 0; i < pts.length; i++) {
        pts[i].rotate(new PVector(tx, ty, tz), new PVector(tx + localZ.x, ty + localZ.y, tz + localZ.z), amt);
        }
        
        break;
        }
         * *
         * *
         */
        validateRotations();
    }

    /*
     * This method will take an orthonornaml basis from the boxes cardinal
     * axes and make the basic euler rotations match
     */
    private void validateRotations() {

        //build an ONB
        PVector u = getLocalYAxis(true);
        PVector f = getLocalZAxis(true);
        PVector r = getLocalXAxis(true);

        //this is the corresponding rotation matrix
        float r11 = r.x;
        float r12 = r.y;
        float r13 = r.z;

        float r21 = u.x;
        float r22 = u.y;
        float r23 = u.z;

        float r31 = f.x;
        float r32 = f.y;
        float r33 = f.z;


        double theta1, psi1, phi1;

        if (r31 != 1) {
            theta1 = -Math.asin(r31);
            double theta2 = Math.PI - theta1;
            psi1 = Math.atan2(r32 / Math.cos(theta1), r33 / Math.cos(theta1));
            double psi2 = Math.atan2(r32 / Math.cos(theta2), r33 / Math.cos(theta2));

            phi1 = Math.atan2(r21 / Math.cos(theta1), r11 / Math.cos(theta1));
            double phi2 = Math.atan2(r21 / Math.cos(theta2), r11 / Math.cos(theta2));
        } else {
            phi1 = 0;
            if (r31 == -1) {
                theta1 = Math.PI / 2;
                psi1 = Math.atan2(r12, r13);
            } else {
                theta1 = -Math.PI / 2;
                psi1 = Math.atan2(-r12, -r13);
            }


        }

        //negate because processing has an upside down coord system

        //remap all rotations to the positive domain
        //this is so we can pack more resolution into fewer bits
        //doesnt matter because boxes dont have front or backs
        //if (psi1 < 0) psi1 = Math.PI+psi1;
        //if (theta1 < 0) theta1 = Math.PI+theta1;
        //if (phi1 < 0) phi1 = Math.PI+phi1;


        if (phi1 > 0) {
            // phi1 = -(Math.PI-phi1);
        }

        rx = (float) -psi1;
        ry = (float) -theta1;
        rz = (float) -phi1;


        //TODO: need to align rotations to bit boundaries



    }

    private void setTri(Triangle tri, float u0, float v0, float u1, float v1, float u2, float v2) {
        tri.u0 = u0;
        tri.v0 = v0;
        tri.u1 = u1;
        tri.v1 = v1;
        tri.u2 = u2;
        tri.v2 = v2;
    }

    public void buildUV() {

        //top
        setTri(tris[0],
                1.f, 0.f,
                0.f, 0.f,
                0.f, 1.f);
        setTri(tris[1],
                0.f, 1.f,
                1.f, 1.f,
                1.f, 0.f);
        tris[0].setUVScale(sx / 100, sz / 100);
        tris[1].setUVScale(sx / 100, sz / 100);

        //right
        setTri(tris[2],
                1.f, 1.f,
                1.f, 0.f,
                0.f, 0.f);
        setTri(tris[3],
                0.f, 0.f,
                0.f, 1.f,
                1.f, 1.f);

        tris[2].setUVScale(sz / 100, sy / 100);
        tris[3].setUVScale(sz / 100, sy / 100);

        //bottom
        setTri(tris[4],
                1.f, 1.f,
                1.f, 0.f,
                0.f, 0.f);
        setTri(tris[5],
                0.f, 0.f,
                0.f, 1.f,
                1.f, 1.f);
        tris[4].setUVScale(sx / 100, sz / 100);
        tris[5].setUVScale(sx / 100, sz / 100);
        //front
        setTri(tris[6],
                1.f, 1.f,
                1.f, 0.f,
                0.f, 0.f);
        setTri(tris[7],
                0.f, 0.f,
                0.f, 1.f,
                1.f, 1.f);
        tris[6].setUVScale(sx / 100, sy / 100);
        tris[7].setUVScale(sx / 100, sy / 100);
        //left
        setTri(tris[8],
                1.f, 1.f,
                1.f, 0.f,
                0.f, 0.f);
        setTri(tris[9],
                0.f, 0.f,
                0.f, 1.f,
                1.f, 1.f);
        tris[8].setUVScale(sz / 100, sy / 100);
        tris[9].setUVScale(sz / 100, sy / 100);
        //back
        setTri(tris[10],
                1.f, 1.f,
                1.f, 0.f,
                0.f, 0.f);
        setTri(tris[11],
                0.f, 0.f,
                0.f, 1.f,
                1.f, 1.f);
        tris[10].setUVScale(sx / 100, sy / 100);
        tris[11].setUVScale(sx / 100, sy / 100);
    }

    //turn the box into a 
    public ArrayList tesselate() {

        Shader shad;
        if (shaders[shaderIndex] instanceof Shader) {
            shad = (Shader) shaders[shaderIndex];
        } else {
            shad = (Shader) shaders[3];
        }

        tris[0] = new Triangle(shad, pts[2], pts[1], pts[0], 0, 0, 0, 0, 0, 0, null);
        tris[1] = new Triangle(shad, pts[0], pts[3], pts[2], 0, 0, 0f, 0, 0, 0f, null);

        tris[2] = new Triangle(shad, pts[6], pts[2], pts[3], 0, 0f, 0f, 0, 0, 0f, null);
        tris[3] = new Triangle(shad, pts[3], pts[7], pts[6], 0, 0, 0, 0f, 0f, 0f, null);

        tris[4] = new Triangle(shad, pts[6], pts[7], pts[4], 0, 0f, 0f, 0, 0, 0, null);
        tris[5] = new Triangle(shad, pts[4], pts[5], pts[6], 0, 0, 0, 0f, 0f, 0f, null);

        tris[6] = new Triangle(shad, pts[7], pts[3], pts[0], 0, 0f, 0, 0f, 0, 0f, null);
        tris[7] = new Triangle(shad, pts[0], pts[4], pts[7], 0, 0f, 0, 0f, 0f, 0f, null);

        tris[8] = new Triangle(shad, pts[4], pts[0], pts[1], 0, 0f, 0, 0f, 0f, 0f, null);
        tris[9] = new Triangle(shad, pts[1], pts[5], pts[4], 0, 0f, 0f, 0f, 0, 0f, null);

        tris[10] = new Triangle(shad, pts[5], pts[1], pts[2], 0, 0, 0, 0f, 0, 0f, null);
        tris[11] = new Triangle(shad, pts[2], pts[6], pts[5], 0, 0, 0, 0, 0, 0, null);

        buildUV();

        ArrayList toRet = new ArrayList();
        for (int i = 0; i < 12; i++) {
            toRet.add(tris[i]);
        }
        return toRet;

    }

    public void pack(UTFWriter output) {

        System.out.println("Writing box at: " + (int) tx + " : " + (int) ty + " : " + (int) tz + " with shaderIndex: " + shaderIndex);

        output.writeBits((int) tx, 8);
        output.writeBits((int) ty, 8);
        output.writeBits((int) tz, 8);

        output.writeBits((int) (sx / 2), 7);
        output.writeBits((int) (sy / 2), 7);
        output.writeBits((int) (sz / 2), 7);

        output.writeBits((int) rot2Int(rx), 6);
        output.writeBits((int) rot2Int(ry), 6);
        output.writeBits((int) rot2Int(rz), 6);

        output.writeBits(shaderIndex, 4);

        output.writeBits(group, 2);

    }

    public String toCode() {
        return "entities.add(new Box(" + (int) tx + "," + (int) ty + ", " + (int) tz
                + ", " + (int) sx / 2 + ", " + (int) sy / 2 + ", " + (int) sz / 2
                + ", " + (int) rot2Int(rx) + ", " + (int) rot2Int(ry) + "," + (int) rot2Int(rz) + ","
                + " shaders, 5));\n";
    }

    public static void writeEmpty(UTFWriter output) {


        output.writeBits(0, 8);
        output.writeBits(0, 8);
        output.writeBits(0, 8);

        output.writeBits(0, 7);
        output.writeBits(0, 7);
        output.writeBits(0, 7);

        output.writeBits(0, 6);
        output.writeBits(0, 6);
        output.writeBits(0, 6);

        output.writeBits(0, 3);


    }

    //turns the float rotation into a 6 bit rotation bounded from 0 to 64
    private int rot2Int(float rot) {
        rot += (float) Math.PI;  //0 to 2PI
        rot /= 2.f * (float) Math.PI;//0 to 1
        rot *= 64;  //0 to 63

        return Math.round(rot);
    }

    private float int2Rot(float rot) {
        rot /= 64;  //0 to 63
        rot *= 2 * (float) Math.PI;//0 to 1
        rot -= (float) Math.PI;  //0 to 2PI
        return rot;
    }

    public void setShader(int shaderIndex) {

        this.shaderIndex = shaderIndex;
        tesselate();

    }

    public PVector getCentroid() {
        return new PVector(tx, ty, tz);
    }
}
